#region License

// Another free, open solution by someone who gets it.
// 
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// 
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
// 
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

#endregion

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;

using Patterns.Validation.Framework;

namespace Patterns.Validation.Advanced
{
	/// <summary>
	/// 	Defines a validator for the <see cref = "PropertyInfo" /> objects returned by the parent <see
	///  	cref = "PropertyInfoSelector" />.
	/// </summary>
	[DebuggerNonUserCode]
	public class PropertyInfoValidator
	{
		/// <summary>
		/// 	Initializes a new instance of the <see cref = "PropertyInfoValidator" /> class.
		/// </summary>
		/// <param name = "properties">The properties.</param>
		public PropertyInfoValidator(IEnumerable<PropertyInfo> properties)
		{
			SubjectProperties = properties;
		}

		/// <summary>
		/// 	Gets the object which value is being asserted.
		/// </summary>
		public IEnumerable<PropertyInfo> SubjectProperties{ get; private set; }

		/// <summary>
		/// 	Validates that the selected properties are virtual.
		/// </summary>
		public AndConstraint<PropertyInfoValidator> BeVirtual()
		{
			return BeVirtual(string.Empty);
		}

		/// <summary>
		/// 	Validates that the selected properties are virtual.
		/// </summary>
		/// <param name = "reason">
		/// 	A formatted phrase as is supported by <see cref = "string.Format(string,object[])" /> explaining why the assertion 
		/// 	is needed. If the phrase does not start with the word <i>because</i>, it is prepended automatically.
		/// </param>
		/// <param name = "reasonArgs">
		/// 	Zero or more objects to format using the placeholders in <see cref = "reason" />.
		/// </param>
		public AndConstraint<PropertyInfoValidator> BeVirtual(string reason, params object[] reasonArgs)
		{
			IEnumerable<PropertyInfo> nonVirtualProperties = GetAllNonVirtualPropertiesFromSelection();

			Execute.Verification
				.ForCondition(!nonVirtualProperties.Any())
				.BecauseOf(reason, reasonArgs)
				.FailWith("Expected all selected properties to be virtual{reason}, but the following properties are" +
				          " not virtual:\r\n" + GetDescriptionsFor(nonVirtualProperties));

			return new AndConstraint<PropertyInfoValidator>(this);
		}

		private PropertyInfo[] GetAllNonVirtualPropertiesFromSelection()
		{
			return SubjectProperties.Where(property => !property.GetGetMethod(true).IsVirtual).ToArray();
		}

		/// <summary>
		/// 	Validates that the selected methods are decorated with the specified <typeparamref name = "TAttribute" />.
		/// </summary>
		public AndConstraint<PropertyInfoValidator> BeDecoratedWith<TAttribute>()
		{
			return BeDecoratedWith<TAttribute>(string.Empty);
		}

		/// <summary>
		/// 	Validates that the selected methods are decorated with the specified <typeparamref name = "TAttribute" />.
		/// </summary>
		/// <param name = "reason">
		/// 	A formatted phrase as is supported by <see cref = "string.Format(string,object[])" /> explaining why the assertion 
		/// 	is needed. If the phrase does not start with the word <i>because</i>, it is prepended automatically.
		/// </param>
		/// <param name = "reasonArgs">
		/// 	Zero or more objects to format using the placeholders in <see cref = "reason" />.
		/// </param>
		public AndConstraint<PropertyInfoValidator> BeDecoratedWith<TAttribute>(string reason, params object[] reasonArgs)
		{
			IEnumerable<PropertyInfo> propertiesWithoutAttribute = GetPropertiesWithout<TAttribute>();

			Execute.Verification
				.ForCondition(!propertiesWithoutAttribute.Any())
				.BecauseOf(reason, reasonArgs)
				.FailWith("Expected all selected properties to be decorated with {0}{reason}, but the" +
				          " following properties are not:\r\n" + GetDescriptionsFor(propertiesWithoutAttribute), typeof (TAttribute));

			return new AndConstraint<PropertyInfoValidator>(this);
		}

		private PropertyInfo[] GetPropertiesWithout<TAttribute>()
		{
			return SubjectProperties.Where(property => !IsDecoratedWith<TAttribute>(property)).ToArray();
		}

		private static bool IsDecoratedWith<TAttribute>(PropertyInfo property)
		{
			return property.GetCustomAttributes(false).OfType<TAttribute>().Any();
		}

		private static string GetDescriptionsFor(IEnumerable<PropertyInfo> properties)
		{
			return string.Join(Environment.NewLine, properties.Select(GetDescriptionFor).ToArray());
		}

		private static string GetDescriptionFor(PropertyInfo property)
		{
			return string.Format("{0} {1}.{2}", property.PropertyType.Name, property.DeclaringType, property.Name);
		}
	}
}